﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Opennav.MapGenerator.OSM;

namespace Opennav.MapGenerator.OSM
{
    public class BinRelationsCache : IEnumerable<OSMRelation>
    {
        private FileStream _relStream;
        private FileStream _idxStream;
        private int idxElementSize = sizeof(uint) + sizeof(long);
        private uint lastId = 0;

        private string relationFile, indexFile;

        public BinRelationsCache(string tempFolder, bool reopen)
        {
            FileMode mode = FileMode.Create;
            if (reopen)
                mode = FileMode.Open;
            relationFile = Path.Combine(tempFolder, "relations.tmp");
            indexFile = Path.Combine(tempFolder, "relations.x");
            _relStream = new FileStream(relationFile, mode, FileAccess.ReadWrite);
            _idxStream = new FileStream(indexFile, mode, FileAccess.ReadWrite);
            if (Count > 0)
            {
                _idxStream.Seek((Count - 1) * idxElementSize, SeekOrigin.Begin);
                lastId = (new BinaryReader(_idxStream)).ReadUInt32();
            }

        }

        public void Close()
        {
            _relStream.Flush();
            _relStream.Close();
            _idxStream.Flush();
            _idxStream.Close();
            File.Delete(relationFile);
            File.Delete(indexFile);
        }

        public void Add(OSMRelation relation)
        {
            if (lastId >= relation.Id)
                throw new InvalidOperationException("relations out of order");
            lastId = relation.Id;
            BinaryWriter idxWriter = new BinaryWriter(_idxStream);
            idxWriter.Seek(0, SeekOrigin.End);
            BinaryWriter relWriter = new BinaryWriter(_relStream);
            relWriter.Seek(0, SeekOrigin.End);
            long offset = relWriter.BaseStream.Position;
            relWriter.Write((ushort)relation.Members.Count);
            foreach (OSMMember member in relation.Members)
            {
                byte type = 0;
                switch (member.Type)
                {
                    case OSMEntityType.Node:
                        type = 1;
                        break;
                    case OSMEntityType.Way:
                        type = 2;
                        break;
                    case OSMEntityType.Relation:
                        type = 3;
                        break;
                }
                relWriter.Write(type);
                relWriter.Write(member.Role);
                relWriter.Write(member.Ref);
            }
            relWriter.Write((ushort)relation.Tags.Count);
            foreach (OSMTag tag in relation.Tags)
            {
                relWriter.Write(tag.Key);
                relWriter.Write(tag.Value);
            }
            idxWriter.Write(relation.Id);
            idxWriter.Write(offset);
        }

        public OSMRelation Get(uint id)
        {
            BinaryReader idxReader = new BinaryReader(_idxStream);
            int middle;
            int left = 0;
            int elements = Count;
            int right = elements - 1;
            while (true)
            {
                middle = left + ((right - left) / 2);
                if (right < left)
                {
                    return null;
                }
                idxReader.BaseStream.Seek(middle * idxElementSize, SeekOrigin.Begin);
                uint elementId = idxReader.ReadUInt32();
                if (elementId == id)
                {
                    return ReadRelation(id);
                }
                if (elementId > id)
                    right = middle - 1;
                else
                    left = middle + 1;
            }
        }

        public int Count
        {
            get
            {
                return (int)(_idxStream.Length / idxElementSize);
            }
        }

        public IEnumerator<OSMRelation> GetEnumerator()
        {
            for (int i = 0; i < Count; i++)
            {
                _idxStream.Seek(i * idxElementSize, SeekOrigin.Begin);
                uint relationId = (new BinaryReader(_idxStream)).ReadUInt32();
                yield return ReadRelation(relationId);
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        private OSMRelation ReadRelation(uint id)
        {
            BinaryReader idxReader = new BinaryReader(_idxStream);
            long offset = idxReader.ReadInt64();
            BinaryReader relReader = new BinaryReader(_relStream);
            relReader.BaseStream.Seek(offset, SeekOrigin.Begin);
            ushort nMembers = relReader.ReadUInt16();
            IList<OSMMember> members = new List<OSMMember>();
            for (int n = 0; n < nMembers; n++)
            {
                byte type = relReader.ReadByte();
                string role = relReader.ReadString();
                uint reference = relReader.ReadUInt32();
                if (type == 1)
                    members.Add(new OSMMember() { Ref = reference, Role = role, Type = OSMEntityType.Node });
                else if (type == 2)
                    members.Add(new OSMMember() { Ref = reference, Role = role, Type = OSMEntityType.Way });
                else if (type == 3)
                    members.Add(new OSMMember() { Ref = reference, Role = role, Type = OSMEntityType.Relation });
                else
                    throw new InvalidDataException();
            }
            ushort ntags = relReader.ReadUInt16();
            IList<OSMTag> tags = new List<OSMTag>();
            for (int ntag = 0; ntag < ntags; ntag++)
            {
                string key = relReader.ReadString();
                string value = relReader.ReadString();
                tags.Add(new OSMTag() { Key = key, Value = value });
            }
            return new OSMRelation() { Id = id, Members = members, Tags = tags };
        }
    }
}
